-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Feature: MCP client Resources support #3024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: MCP client Resources support #3024
Conversation
…r to return these in preference to `mcp` upstream types.
|
I was just looking at this as I need resources support, and noticed this doesn't provide any way to determine whether the server even supports resources. The only way to tell with this code would be to attempt to make a resource call (e.g. I think this should be better exposed as I would like my application to switch to alternate behavior if the MCP server does not support resources. And would like to do so without having to make a call and see if it fails, and then differentiate whether that failure was because of lack of support, or some other reason. The MCP spec says that the |
|
Another thing I noticed going through the code, is that for I'm not sure if addressing this is even in scope of this PR, as |
That's a great point (and not something I was looking at in my changes). Capabilities presumably hasn't been an issue up until this point because MCP was only used for tools in Pydantic AI and it was assumed you wouldn't use an MCP server unless it had tool capabilities. I'm not quite sure the right way to support this. The upstream There is an existing pattern, which is how @property
def server_info(self) -> mcp_types.Implementation:
"""Access the information send by the MCP server during initialization."""
if getattr(self, '_server_info', None) is None:
raise AttributeError(
f'The `{self.__class__.__name__}.server_info` is only instantiated after initialization.'
)
return self._server_infoWe could follow this pattern and just return Maybe something like: ServerCapabilities = Literal[
"experimental",
"logging",
"prompts",
"resources",
"tools",
"completions"
]
# MCPServer
@property
def capabilities() -> set[ServerCapabilities]:
...Meh, I guess that's kind ugly: I don't want this PR to drag on any longer than it already has, but this seems like a reasonable/incremental change overall? |
|
What happens when you try to retrieve resources from a server that doesn't have that capability? |
Right now (current state) it would raise an Arguably we could choose to intercept this and fail-early if the server doesn't have the capability, but I'm not sure it really adds any value? There's also not much of a pattern in the codebase for this that I can tell (given how lightweight I think we can just leave it up to the client application/user to check which server capabilities exist via something like a |
|
Since "resources are not supported" or "the requested resource doesn't exist" are such common cases users will typically need to handle, I think they should be our own exceptions or the methods could just return |
I've done some additional testing. The specifics of errors are pretty server-implementation specific. For example, the official python-sdk's FastMCP server doesn't even have the ability to disable
I think returning For other errors in things like I'll take a pass at this direction to review. |
|
I'm fine with that. |
|
This is ready for re-review. I noticed we're consistently getting test failures on a specific subset of test suites, but they really seem unrelated to these changes as far as I can see? Example here. |
| @classmethod | ||
| def from_mcp_sdk_error(cls, error: mcp_exceptions.McpError) -> MCPError: | ||
| """Create an MCPError from an MCP SDK McpError. | ||
| Args: | ||
| error: An McpError from the MCP SDK. | ||
| Returns: | ||
| A new MCPError instance with the error data. | ||
| """ | ||
| # Extract error data from the McpError.error attribute | ||
| error_data = error.error | ||
| return cls(message=error_data.message, code=error_data.code, data=error_data.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just because we're returning our own/native MCPError type, rather than the mcp-sdk one and we're flattening it out. The MCP SDK error type has a nested error element as it exactly mirrors the protocol JSON, whilst ours is flattened out.
Apologies if I've misinterpreted your question 🙂.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure this is necessary, since the developer has access to the exception cause, since we are doing raise ... from e.
Do we have a test demonstrates how useful this would be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mostly added the ... from e to preserve the originating stack trace. You are right though, aside from the stack trace, our native exceptions literally replicate the data in the original exception 1:1.
If we don't think the originating stack trace is useful, I can remove it.
I still think it's useful to have the actual error details in the exception being thrown because it's easier for the user to access for "normal" error handling, eg:
try:
...
except MCPError as e:
print(f'Something terrible happened reading your resource: {e.message} (err: {e.code})')Lemme know if I'm missing something!
pydantic_ai_slim/pydantic_ai/mcp.py
Outdated
| try: | ||
| result = await self._client.read_resource(AnyUrl(resource_uri)) | ||
| except mcp_exceptions.McpError as e: | ||
| # As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can explain a bit more here...
| # As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling | |
| # As per https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling | |
| # `-32002` defines "resource not found". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with @DouweM regarding raising an error vs returning None.
In this method, we can already see that None means different things:
- resource is not supported by the server.
- resource not found.
- no content in resource.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think we can leverage the exceptions on the future exception handler mechanism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with @DouweM regarding raising an error vs returning
None.
Yeah, I can see arguments in both directions. FWIW, I think returning [] or None when the server doesn't have resource capabilities is reasonable, ie: list_resources() returning [] is materially the same and if you can't get any resource URIs, then you can't really call read_resource() anyway. I'm not sure raising an exception here adds much value for the user.
If a client really needs to know if a server supports resources (maybe to display UI elements or something) you can still access mcp_server.capabilities.
On returning None vs a ResourceNotFound - This is trickier. One related issue we have is that so far, none of the main reference MCP server implementations (typescript, python) actually implement the protocol spec correctly anyway (though it looks like a python fix might be on the way), which means it's hard to detect a real "not found".
FWIW 2, our closest related precedence for this is probably direct_tool_call(), however, it raises ModelRetry regardless of the actual underlying error, which arguably is also not correct if a user is calling it directly?
Maybe the right model for this is (roughly) how HTTP error codes are commonly handled? Rather than having different subclasses (ie: ResourceNotFound, ServerError, etc) we raise a generic error (maybe just MCPError), which has a code and a message and let users handle flow based on the specific codes they care about (much like users would with HTTP response codes 400-599)? ie:
try:
data = server.read_resource('url://here')
except MCPError as e:
print(f'An error occurred reading your resource: {e.message} (err: {e.code})')Maybe even define some useful common code maps?
RESOURCE_NOT_FOUND = -32002
INVALID_REQUEST = -32600
@Kludex you would have a better sense than I on this sort of thing 😄
Re:
Also, I think we can leverage the exceptions on the future exception handler mechanism.
Sorry, not quite sure what you mean by this - could you clarify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just when you thought this couldn't get even more annoying, I just stumbled on this: modelcontextprotocol/modelcontextprotocol#1545
It turns out the spec itself is wrong/has a typo
This is probably a vote in the direction of raising errors with codes and letting clients work out what they want to do with them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kludex gentle nudge?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there are stable version of PydanticAI with MCP Resources ready to use yet? If not when will you have something stable with basic MCP resource functionality released?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy with list_resources() returning empty list.
And I'm fine with this one returning MCPError instead of None. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thank you! I’ll proceed with this asap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy with
list_resources()returning empty list.And I'm fine with this one returning
MCPErrorinstead ofNone. 👍
Both done as of latest changes!
…r than try to cast to None | []
|
@Kludex - This is ready for re-review. Changes should be implemented as proposed/discussed here (along with addressing the other prior review comments). Let me know if there are any concerns/changes desired! |
|
FWIW, the test failure on 3.12 appears unrelated to my changes (though seems to occur reliably). |
|
@fennb Thanks Fenn, I've gone ahead and merged it and we'll see if the error occurs on main (I've also seen it happen somewhere in between flaky and reliably depending on the PR :/) |
Thanks @DouweM @Kludex for your help/guidance on this one (and sorry it dragged on a bit). Let me know if there's anything else (related or otherwise) I can help out with. My general plan is to turn my attention to #3099 next. |
Addresses #1783.
Adds support for MCP Resources to
MCPServer.Changes
Resource Methods
list_resources()- Discover available resources on the serverlist_resource_templates()- List parameterized resource templates (RFC 6570)read_resource(uri)- Read resource contents, accepting string URI or Resource objectNative Types
Resource,ResourceTemplate,ResourceAnnotations, andServerCapabilitiestypesstrand binary content toBinaryContentmcplibrary types in the public APIError Handling
MCPErrorOther
MCPServer.capabilitiesproperty to inspect server featuresmcp>=1.18.0(required for Annotations tests)